package service

import (
	"fmt"
	"happy-sudoku/internal/codec"
	"happy-sudoku/internal/model"
	"happy-sudoku/internal/repository"
	"happy-sudoku/internal/util"
	"strings"

	jwt "github.com/dgrijalva/jwt-go"
	"github.com/pkg/errors"
)

// IAuthService ...
type IAuthService interface {
	// ValidateTokenAndGet validates token and return playerID
	ValidateTokenAndGet(token string) (*model.Auth, error)
	// GenerateToken generate token for player
	GenerateToken(auth model.Auth) (string, error)
}

// AuthJwtService implements IAuthService, auth by jwt
type AuthJwtService struct {
	jwtSecret []byte
}

// ExtractJWTToken extract token converts `Bearer "<token>"` to `token`
func ExtractJWTToken(tokenStr string) string {
	idx := strings.Index(tokenStr, "\"")
	// normal string
	if idx == -1 {
		return tokenStr
	}
	// extract, (the last is another \")
	return tokenStr[idx+1 : len(tokenStr)-1]
}

// NewAuthJwtService new jwt auth service
func NewAuthJwtService(jwtSecret []byte) *AuthJwtService {
	// use uuid for default secret, will be diffrent every call or
	// restart program
	// !! TODO: Save it in database (distributed system)
	return &AuthJwtService{
		// jwtSecret: []byte(util.NewUUIDString()),
		// for test use
		jwtSecret: jwtSecret,
	}
}

// ValidateTokenAndGet validates token and return playerID
func (a *AuthJwtService) ValidateTokenAndGet(tokenStr string) (*model.Auth, error) {
	tokenStr = ExtractJWTToken(tokenStr)
	if tokenStr == "" {
		return nil, codec.ErrInvalidToken.Wrap("token must not be empty")
	}

	token, _ := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return a.jwtSecret, nil
	})

	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		var auth model.Auth
		err := util.MapToStruct(claims, &auth)
		if err != nil {
			return nil, errors.Errorf("Jwt token has unmatched claims: %+v", token.Claims)
		}
		return &auth, nil
	}

	return nil, codec.ErrInvalidToken.WithField("token", tokenStr)
}

// GenerateToken generate token for player
func (a *AuthJwtService) GenerateToken(auth model.Auth) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(util.StructToMap(auth)))
	tokenStr, err := token.SignedString(a.jwtSecret)
	if err != nil {
		return "", err
	}

	return tokenStr, nil
}

// AuthCookieService implements IAuthService
type AuthCookieService struct {
	repo repository.IAuthRepo
}

// NewAuthCookieService new auth service
func NewAuthCookieService(repo repository.IAuthRepo) *AuthCookieService {
	return &AuthCookieService{
		repo: repo,
	}
}

// ValidateTokenAndGet validates cookie and return playerID
func (a *AuthCookieService) ValidateTokenAndGet(token string) (int, error) {
	auth, err := a.repo.GetOneByCookie(token)
	if err != nil {
		return 0, err
	}
	return auth.PlayerID, nil
}

// GenerateToken generate cookie for player
func (a *AuthCookieService) GenerateToken(playerID int) (string, error) {
	auth := model.MakeAuth()
	auth.PlayerID = playerID
	// the uuid generated only use in cookie, it's not same as login token
	cookieToken := util.NewUUIDString()
	err := a.repo.CreateByCookie(cookieToken, auth)
	if err != nil {
		return "", err
	}
	return cookieToken, nil
}
